在研究时间调度时就有 React 的异步渲染的迹象了,只是在实际应用中却不知道如何开启,如何跟踪,后来看了 react-fiber-resources 上面介绍的 Try React Fiber asynchronous rendering!。其实在 React 的博客 React v16.0 发布之际就已经说明了:

We think async rendering is a big deal, and represents the future of React. To make migration to v16.0 as smooth as possible, we’re not enabling any async features yet, but we’re excited to start rolling them out in the coming months. Stay tuned!

直到现在本文研究的版本 16.4.1,异步渲染 async rendering 仍然没有正式推出,其中日常开发中可以看到 ReactDOM.render(element, container) 模式采用的就是 legacyCreateRootFromDOMContainer 这类方式,在生成 root 的时候直接将 root.current 的 mode 赋值为 0,也就是常见的同步的方式。fiber 的模式 mode 有以下类型:

// ReactTypeOfMode.js
const NoContext = 0b000;
const AsyncMode = 0b001;
const StrictMode = 0b010;
const ProfileMode = 0b100;

StrictMode/ProfileMode 在源码中似乎是用于开发中的,于是看这里的 AsyncMode 模式,也就是异步模式。只是通过官网教程介绍的方式是无法开启异步模式,还没有大规模的推广,现在日常开发用的 React 基本上还是同步模式 sync,也就是上面的 NoContext。

那如何开始异步模式呢?在上文链接中有介绍到。可以通过以下方式:

// 较于之前的方式,将 container 与 element 分开。通过 unstable_createRoot 
// 创建异步的 fiber。
const root = ReactDOM.unstable_createRoot(container);
root.render(<App />);

unstable_createRoot 的方式和先前一样,创建根部 root,即是 ReactRoot 对象,当前 current fiber 为异步模式,mode 为 3 即 AsyncMode|StrictMode。再进行 rendering 渲染。

expirationTime

在早期的 React 版本里面采用了优先级的 Priority 模式,如下:

module.exports = {
  NoWork: 0,              // No work is pending.
  SynchronousPriority: 1, // For controlled text inputs. Synchronous side-effects.
  AnimationPriority: 2,   // Needs to complete before the next frame.
  HighPriority: 3,        // Interaction that needs to complete pretty soon to feel responsive.
  LowPriority: 4,         // Data fetching, or result from updating stores.
  OffscreenPriority: 5,   // Won't be visible but do the work in case it becomes visible.
};

到了该版本 React 16.4.1,采用的则是 fiber.mode 以及 expirationTime 相继结合来实现异步以及优先级的。已经没有上面这些标志的存在了。通过统一的参数 expirationTime 来表示优先级。分为两种情况:

  1. expirationTime 为 1,就是同步模式,也就类似于 React 16 之前的 stack 情况,程序会按照顺序执行下去,直到结束;
  2. expirationTime 不为 1 的时候,则其值越低,优先级越高。rootExpirationTime 不为 1 的时候会启动异步模式。
function computeExpirationForFiber(currentTime: ExpirationTime, fiber: Fiber) {
  let expirationTime;
  if (expirationContext !== NoWork) {
    expirationTime = expirationContext;
  } else if (isWorking) {
    if (isCommitting) {
      // 在 commit 阶段更新任务,需要给予同步优先级
      expirationTime = Sync;
    } else {
      // 渲染阶段则设置为渲染结束后的下次时间
      expirationTime = nextRenderExpirationTime;
    }
  } else {
    // 非执行阶段,根据 fiber.mode来设置 expirationTime。
    if (fiber.mode & AsyncMode) {
      // 异步模式下
      if (isBatchingInteractiveUpdates) {
        expirationTime = computeInteractiveExpiration(currentTime);
      } else {
        expirationTime = computeAsyncExpiration(currentTime);
      }
    } else {
      // 同步模式下
      expirationTime = Sync;
    }
  }
  // ...省略交互相关部分
  return expirationTime;
}

如果 fiber.mode === AsyncMode,则 expirationTime 为大于 1 的值,同步则为 1,当然同步是优先级最高的,异步具有 LowPriority。前面博客都是同步模式下研究的。对于异步时间的计算,按照 当前时间为基准,250ms 为一个单元。两个异步 fiber 计算 expirationTime 的时候,如果当前时间 currentTime/25 为相同整数,则这两个异步 fiber 具有相同的优先级。

render/reconciliation 阶段的从深调用栈返回处理交互事件,也和时间的计算有关系。在上文中提到,在 workLoop 中通过计算 deadline.timeRemaining() 来判断是否还剩余时间,这个也就是 requestIdleCallback 的用法。但是有一点不同,在 performWork 里面,也就是即将进入 performWorkOnRoot 之前有:

function performWork(minExpirationTime, dl) {
  //...省略部分
  if(dl !== null) {
    while (
      nextFlushedRoot !== null &&
      nextFlushedExpirationTime !== NoWork &&
      (minExpirationTime === NoWork ||
        minExpirationTime >= nextFlushedExpirationTime) &&
      (!deadlineDidExpire ||
        recalculateCurrentTime() >= nextFlushedExpirationTime)
    ) {
      recalculateCurrentTime();
      performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime, true);
      findHighestPriorityRoot();
    }
  }
  // 省略部分
}

由前文可知当没有剩余时间的时候会终结 workLoop 循环,同时令 deadlineDidExpire = true,只是如果这样还是不足以使得 performWork 里面的循环为 false,必须要 recalculateCurrentTime() >= nextFlushedExpirationTime。recalculateCurrentTime 计算的是当前时间,nextFlushedExpirationTime 则是 root.expirationTime,初看的时候会自然的认为后者肯定是更小的,但是恰恰相反。异步的 expirationTime 是以 5000ms 为基数, 250ms 为单位计算的。所以只有当超出时间过长,比如五六秒,才会变为立刻执行的情况。在之前的小版本里面,基本是只要是剩余时间不足就立刻中断,没有和 nextFlushedExpirationTime 比较的过程。

前面的 expirationContext 涉及到 ReactDOM 的另一个异步更新的 API,如下:

ReactDOM.unstable_deferredUpdates(() => {
  ReactDOM.render(<App />. container);
  // or
  instance.setState(() => newState);
}); 

与 unstable_createRoot 一开始就创建异步模式的 fiber 不同,unstable_deferredUpdates 会修改 expirationContext 生成异步的 expirationTime 达到延迟更新的目的,

优先级

expirationTime !== SYNC 时候,fiber 就具有不同的优先级,有优先级的前提是异步。异步模式是通过时间调度方式实现的,具体看前文 react 时间调度。那优先级是定义好了,但是如何保证高优先级的先执行呢?

在 renderRoot 的时侯,会有 nextRenderExpirationTime = root.nextExpirationTimeToWorkOn,所以当初次构建 workInProgress Tree 的时候所有的 fiber 也就是 expirationTime 都是一致,哪怕是异步组件 React.unstable_AsyncMode。生产的 expriationTime 是一样的,但是若中途出现优先级更高的事件,导致基准的 expriationTime 改变又如何?

function beginWork(current, workInProgress, renderExpirationTime) {
  if (
    workInProgress.expirationTime === NoWork ||
    workInProgress.expirationTime > renderExpirationTime
  ) {
    return bailoutOnLowPriority(current, workInProgress);
  }
  // ...省略后面 switch (workInProgress.tag) 的情况
}

在 beginWork 就比较当前 fiber.expirationTime 和 renderExpirationTime 关系。renderExpirationTime 即是 nextRenderExpirationTime,是下次要渲染的时长,高于它,则该 fiber 将不会在该阶段中渲染。这种情况会发生在异步渲染里面,还在渲染阶段的时候,插入低优先级的的事件,这个时候将会产生更低优先级的 fiber,本轮 commit 结束后,会在下一次渲染阶段再做安排。

update 的优先级

在 fiber 里面有任务队列 updateQueue,该队列维护的是传参 element,是个链表结构,节点为 update。通过 update.nextEffect 来指向下一个节点。而这里的每个节点,则是需要更新的 element。update 结构如下:

export function createUpdate(expirationTime: ExpirationTime): Update<*> {
  return {
    expirationTime: expirationTime,
    tag: UpdateState,
    payload: null,
    callback: null,
    next: null,
    nextEffect: null,
  };
}

expirationTime 是对应该节点的优先级。fiber 有优先级,这个 update 也有优先级,这个在 ReactUpdateQueue.js 开头的注释有介绍到:

  1. updates 的排序不是基于优先级的,而是基于插入顺序。
  2. 渲染阶段的时候,只有高优先级的 update 才会被被用到。

以前版本里面,对 update 的处理是采用 insert 方式的,即是高优先级的会插入到低优先级 update 前面。而今则是通过 expirationTime 区分。

function processUpdateQueue(workInProgress, queue, props, instance, renderExpirationTime){
  // ...省略部分
  let update = queue.firstUpdate;
  let resultState = newBaseState;
  while (update !== null) {
    const updateExpirationTime = update.expirationTime;
    if (updateExpirationTime > renderExpirationTime) {
      // This update does not have sufficient priority. Skip it.
      if (newFirstUpdate === null) {
        newFirstUpdate = update;
        newBaseState = resultState;
      }
    } else {
      // This update does have sufficient priority. Process it and compute
      // a new result.
      // ..,省略
    }
  }
  // ..,省略
}

可以看出只有小于或者等于 renderExpirationTime 的才会被采用,你没有看错是小于或者等于,而不仅仅是小于。因为同优先级的 update 是会被采用的,这个 bug 已经在 Always batch updates of like priority within the same event 被修复了,well,同时也修改和很多地方,最大的是 requestCurrentTime 取代了 recalculateCurrentTime。 没有被采用的 update 最怎么样呢?通过 newFirstUpdate 保留到 updateQueue 里的 firstUpdate 了。下次渲染的时候,才会被用到。

总结

异步渲染里面,除了 ReactDOM.unstable_createRoot/ReactDOM.unstable_deferredUpdates 以外,还有 React.unstable_AsyncMode 的方式创建异步组件,React.Timeout 这样的 TimeoutComponent 组件。只是目前关于异步,优先级,React 本身还在一直更新中,指不定有不少的坑,当然都冠以 unstable 的名号了,谁用谁负责,就像 UNSAFE_componentWillReceiveProps 之类的。期待未来 React 异步渲染正式推出那天。期待下图:

优先级里面还有更多的是生成 workInprogress tree 的时候也会设置,包括是否是 offscreen/hidden,这里就不做研究了。

参考

  1. [翻譯] React Fiber 現狀確認
  2. react-fiber-resources 里面的资源都灰常好!如这篇 Build your own React Fiber,还有视频系列。
  3. coordinating-async-react